[Swift] OptionSetについてまとめてみます
はじめに
モバイルアプリサービス部の中安です。
今回は、使うと非常に便利。Swift標準ライブラリにある OptionSet
についてまとめようかと思います。
OptionSet
OptionSet
は Swift標準ライブラリに搭載されているプロトコルのひとつです。
かつては OptionSetType
という名前で定義されていました。
リファレンスの説明にはこのように記載されています。
ビット集合型を表現するためOptionSetを使用します。個々のビットは集合のメンバーを表現します。
英訳は少し怪しいですが(苦)、ビットの集合を表すという役割が OptionSet
に準拠した型には求められます。
これをもう少し噛み砕くと、プログラミングではお馴染みの「ビット演算」を容易に扱うことができるのが OptionSet
になります。
ビット演算の詳しいところは他記事に委ねるとして「1つの値で複数のフラグが扱えるもの」という解釈で大丈夫かと思います。 では、iOSアプリ開発において「1つの値で複数のフラグが扱える」というのが具体的にはどういうシーンで使われているかを見てみたいと思います。
例: UIControl.State
iOSアプリ開発で頻出するOptionSet
の例としては、UIControl
の内部で定義されているUIControl.State
があると思います。
UIコンポーネントの「通常時」「選択時」「使用不可時」などを表現する型です。
そのUIControl.State
は、下記のようにOptionSet
に準拠する形で定義がなされています。
extension UIControl { public struct State : OptionSet { public init(rawValue: UInt) public static var normal: UIControl.State { get } public static var highlighted: UIControl.State { get } public static var disabled: UIControl.State { get } public static var selected: UIControl.State { get } public static var focused: UIControl.State { get } public static var application: UIControl.State { get } public static var reserved: UIControl.State { get } } }
たとえばUIButton
はUIControl
の継承クラスなので、
ボタンをソースコードから生成する場合、そのボタンの文言を設定する際は下記のように書きます。
let button = UIButton(type: .custom) button.setTitle("ボタン", for: .normal)
setTitle
の第2引数は、UIControl.State
です。ここに「通常時の文言」という意味で、.normal
を突っ込んでいるわけですが、
下記のようにも書くことができます。
let button = UIButton(type: .custom) button.setTitle("ボタン", for: [.normal, .highlighted])
こうすることで「通常時の文言」と「ハイライト時の文言」を同時に設定ができます。
配列の形で渡しているので2つの値を渡しているように見えますが、OptionSet
の機能により「複数のフラグを有した1つの値」として扱われます。
どういうことなのかを実際に試してみます。
let state: UIControl.State = [.normal, .highlighted, .disabled]
変数state
の型はUIControl.State
ですが、渡しているのは配列の形式です。
しかしこれはコンパイルエラーにはなりません。
normal
とhighlighted
とdisabled
のビット値が合計された状態で変数に代入されるというイメージです。
当然ですが、下記のようなソースコードを書くとコンパイルエラーになります。String
に対して [String]
を渡しているからですね。
let state: String = [".normal", ".highlighted", ".disabled"] // エラー
OptionSet
が少し特別な型だというのがわかると思います。
例: UIControl.Event
同じUIControl
内にあるUIControl.Event
型もまたOptionSet
になります。
定義は以下のような感じです。数が多いので一部省略しています。
extension UIControl { public struct Event : OptionSet { public init(rawValue: UInt) public static var touchDown: UIControl.Event { get } public static var touchDownRepeat: UIControl.Event { get } public static var touchDragInside: UIControl.Event { get } public static var touchDragOutside: UIControl.Event { get } public static var touchDragEnter: UIControl.Event { get } public static var touchDragExit: UIControl.Event { get } public static var touchUpInside: UIControl.Event { get } public static var touchUpOutside: UIControl.Event { get } public static var touchCancel: UIControl.Event { get } public static var valueChanged: UIControl.Event { get } // (中略) public static var allTouchEvents: UIControl.Event { get } public static var allEditingEvents: UIControl.Event { get } // (中略) public static var allEvents: UIControl.Event { get } } }
ここではOptionSet
の便利な使い方である「フラグの合成」の仕組みが使われています。
allTouchEvents
やallEvents
などがそれに当たります。
allTouchEvents
では touch〜
のすべてのイベントを表現します。
しかし、これはフラグが再定義されているわけではなく、touch〜
型のすべてのビットフラグを立てた状態で定義されています。
これについては後述しますが、ビット演算の恩恵として良い例かと思います。
例: UNAuthorizationOptions
プッシュ通知の承認処理で使用する UNAuthorizationOptions
もまた OptionSet
の仲間です。
public struct UNAuthorizationOptions : OptionSet { public init(rawValue: UInt) public static var badge: UNAuthorizationOptions { get } public static var sound: UNAuthorizationOptions { get } public static var alert: UNAuthorizationOptions { get } public static var carPlay: UNAuthorizationOptions { get } @available(iOS 12.0, *) // 省略 }
通知センターにバッジ、サウンド、アラートを個別な値で渡しているのではなく、ビットのフラグとして渡しているのです。
自分で定義してみる
ここまでは既に提供されているOptionSet
の仲間を見てきましたが、自分で定義することも簡単にできます。
再びリファレンスの説明を見てみます。
OptionSetを作成するとき、型の定義にrawValueプロパティを含めます。
For your type to automatically receive default implementations for set-related operations, the rawValue property must be of a type that conforms to the FixedWidthInteger protocol, such as Int or UInt8.
型がデフォルトの集合関連操作の実装を自動的に受けるには、rawValueプロパティがIntやUInt8のようなFixedWidthIntegerプロトコルに準拠した型でなければなりません。
Next, create unique options as static properties of your custom type using unique powers of two (1, 2, 4, 8, 16, and so forth) for each individual property’s raw value so that each property can be represented by a single bit of the type’s raw value.
その次に、個々のプロパティに一意である2の累乗(1, 2, 4, 8, 16等々)の元値を使用したカスタムな型の静的プロパティを一意なオプションとして作ってください。そうすることで、それぞれのプロパティは型の単一ビットの元値を表現することができます。
これまた英訳怪しいですが(苦)、要約すると下記のようなことです。
- カスタムな
OptionSet
の型を定義する時はrawValue
プロパティを定義する rawValue
の型はInt
やUInt
を使用すること- 静的なプロパティを定義して、それぞれに一意な
rawValue
を定義する rawValue
は2の累乗である必要がある- そうすることで自動的に
OptionSet
の機能が付与される
例: パーミッション
では実際に上の要件に合わせて作ってみるのですが、
イメージしやすいのは Linuxコマンドのchmod
でもおなじみなパーミッションかなと思ったのでそれでやってみます。
struct Permission : OptionSet { // (1) let rawValue: UInt // (2) static let executable = Permission(rawValue: 1 << 0) // (3) static let writable = Permission(rawValue: 1 << 1) static let readable = Permission(rawValue: 1 << 2) }
(1) OptionSetを実装する
書いたとおり、Permission
という構造体を定義してOptionSet
に準拠します。
ちなみにOptionSet
である型はそれ自体が機能を有するものではない(自動的に付与されるものを除いて)ので、クラスよりは構造体で定義したほうが良いかと思います。
(2) rawValueプロパティを定義する
リファレンスにある通りrawValue
プロパティを定義します。これを定義しないと、OptionSet
プロトコルの準拠に適していないというエラーが出てきます。
ここでは UInt
としていますが、項目が少なければキャストの面倒臭さを避けるために Int
でもいいと思います。
(3) 静的プロパティで項目を定義する
パーミッションの項目を静的プロパティで定義します。
この例では、Linux でxwr
に相当する「実行権限」「書き込み権限」「読み取り権限」を作成しています。
rawValue
には2の累乗を渡すとされているので、1, 2, 4
と直接な値を渡してもいいのですが、
ここは「左ビットシフト演算子(<<
)」を使うほうが可読性やメンテナンス性的にはよさそうです。
使ってみる
実際に使ってみた例がこちらになります。rawValue
を出力してみると 1
が表示されると思います。
let permission: Permission = .executable print(permission.rawValue)
単一ではなく複数のフラグを立てた状態にするには、前述したように配列形式で渡します。
let permission: Permission = [.executable, .readable] print(permission.rawValue)
ここでの出力は executable = 1
と readable = 4
で、5
が出力されます。
大事なのは出力された数値ではなく、実態として2進数によって 最初の例は 001
となり、次の例では 101
となっていることです。
ちなみに、配列の形式になっているからといって変数の定義をこのようにしないでください。意味が変わってきてしまいます。
let permission: [Permission] = [.executable, .readable]
使い方
最後にOptionSet
の使い方についてです。
フラグが立っているかどうか
指定したフラグが立っているかどうかは、contains
メソッドで判定できます。
let permission: Permission = [.executable, .readable] // 5 = 101 permission.contains(.executable) // true permission.contains(.writable) // false permission.contains(.readable) // true
2進数の値は、101
なのでwritable
フラグは立っていないという結果になります。
let permission: Permission = [.executable, .readable] // 5 = 101 permission.contains([.executable, .readable]) // true permission.contains([.executable, .writable]) // false
このように複数のフラグを一度に判定することもできます。 この際の判定は AND で判定されることも結果からわかると思います。
フラグの更新
すでに値がセットされたOptionSet
のフラグ値を変更することができます。
構造体で定義されている場合は変数宣言をvar
でしておく必要があります。
フラグを立てる
フラグを立てるには insert
メソッドで行います。下記の例でフラグが足されていることが確認できると思います。
var permission: Permission = .executable permission.rawValue // 1 = 001 permission.insert(.readable) permission.rawValue // 5 = 101 permission.insert(.writable) permission.rawValue // 7 = 111 permission.insert(.readable) permission.rawValue // 7 = 111
当然のことながら、すでに立っているフラグに対して更にinsert
してもrawValue
は変わりません。
フラグを降ろす
フラグを降ろすには remove
メソッドで行います。下記の例でフラグが減っていることが確認できると思います。
var permission: Permission = [.executable, .writable, .readable] permission.rawValue // 7 = 111 permission.remove(.readable) permission.rawValue // 3 = 011 permission.remove(.writable) permission.rawValue // 1 = 001 permission.remove(.readable) permission.rawValue // 1 = 001 permission.remove(.executable) permission.rawValue // 0 = 000
当然のことながら、すでに降りているフラグに対して更にremove
してもrawValue
は変わりません。
また、すべてのフラグが降りると0になります。
ビットセット(集合)同士の計算
OptionSet
がビット演算を扱う性質であるゆえに集合の演算も機能として用意されています。
和集合
100
と001
というビットセットがある場合、和集合(OR
)で演算すると101
となります。
この和集合を表現するのがunion
メソッドです。
let permission1: Permission = .executable // 1 = 001 let permission2: Permission = .readable // 4 = 100 permission1.union(permission2).rawValue // 5 = 101
積集合
100
と101
というビットセットがある場合、積集合(AND
)で演算すると100
となります。
この積集合を表現するのがintersection
メソッドです。
let permission1: Permission = .readable // 4 = 100 let permission2: Permission = [.executable, .readable] // 5 = 101 permission1.intersection(permission2).rawValue // 4 = 100
差集合
111
と101
というビットセットがある場合の差集合は010
となります。
この差集合を表現するのがsubtracting
メソッドです。
let permission1: Permission = [.executable, .readable, .writable] // 7 = 111 let permission2: Permission = [.executable, .readable] // 5 = 101 permission1.subtracting(permission2).rawValue // 2 = 010
合成の定義
UIControl.Event
の項で allTouchEvents
やallEvents
について書きました。
実装上その合成されたOptionSetの値に意味を付けたいときは、定義として作成しておくほうが良いかと思います。
struct Permission : OptionSet { let rawValue: UInt static let executable = Permission(rawValue: 1 << 0) static let writable = Permission(rawValue: 1 << 1) static let readable = Permission(rawValue: 1 << 2) static let full: Permission = [.executable, .readable, .writable] }
このように定義しておくことで、「権限をすべて持ち合わせている」という意味がわかりやすくなるのではないでしょうか。
最後に
いくつものフラグを管理することも多いかと思いますが、
OptionSet
の仕組みを上手く使えば、1つの整数値でいくつものフラグを管理することができます。
例えばRealm
などのデータベースにいくつもプロパティ(カラム)を作るのではなく、1つのInt
値で管理することもできます。
もちろん、なんでもかんでも適しているというわけではなく、使える条件は見極める必要はありますが 設計する上で選択肢の引き出しの一つとして持っておくことも大事かと思います。
何かの参考になれば幸いです。